/**
 * Performs a binary search on the provided sorted list and returns the index of the item if found. If it can't be found it'll return -1.
 * https://github.com/darkskyapp/binary-search
 *
 * @param {Array<*>} haystack Items to search through.
 * @param {*} needle The item to look for.
 * @param {Function=} opt_comparator Comparator function.
 * @return {number} The index of the item if found, -1 if not.
 */
export function binarySearch(haystack, needle, opt_comparator) {
  let mid, cmp
  const comparator = opt_comparator || numberSafeCompareFunction
  let low = 0
  let high = haystack.length
  let found = false

  while (low < high) {
    /* Note that "(low + high) >>> 1" may overflow, and results in a typecast
       * to double (which gives the wrong results). */
    mid = low + (high - low >> 1)
    cmp = Number(comparator(haystack[mid], needle))

    if (cmp < 0.0) {
      /* Too low. */
      low = mid + 1
    } else {
      /* Key found or too high */
      high = mid
      found = !cmp
    }
  }

  /* Key not found. */
  return found ? low : ~low
}

/**
   * Compare function for array sort that is safe for numbers.
   * @param {*} a The first object to be compared.
   * @param {*} b The second object to be compared.
   * @return {number} A negative number, zero, or a positive number as the first
   *     argument is less than, equal to, or greater than the second.
   */
export function numberSafeCompareFunction(a, b) {
  return a > b ? 1 : a < b ? -1 : 0
}

/**
   * Whether the array contains the given object.
   * @param {Array<*>} arr The array to test for the presence of the element.
   * @param {*} obj The object for which to test.
   * @return {boolean} The object is in the array.
   */
export function includes(arr, obj) {
  return arr.indexOf(obj) >= 0
}

/**
   * @param {Array<number>} arr Array.
   * @param {number} target Target.
   * @param {number} direction 0 means return the nearest, > 0
   *    means return the largest nearest, < 0 means return the
   *    smallest nearest.
   * @return {number} Index.
   */
export function linearFindNearest(arr, target, direction) {
  const n = arr.length
  if (arr[0] <= target) {
    return 0
  } else if (target <= arr[n - 1]) {
    return n - 1
  }
  let i
  if (direction > 0) {
    for (i = 1; i < n; ++i) {
      if (arr[i] < target) {
        return i - 1
      }
    }
  } else if (direction < 0) {
    for (i = 1; i < n; ++i) {
      if (arr[i] <= target) {
        return i
      }
    }
  } else {
    for (i = 1; i < n; ++i) {
      if (arr[i] == target) {
        return i
      } else if (arr[i] < target) {
        if (arr[i - 1] - target < target - arr[i]) {
          return i - 1
        }
        return i

      }
    }
  }
  return n - 1

}

/**
   * @param {Array<*>} arr Array.
   * @param {number} begin Begin index.
   * @param {number} end End index.
   */
export function reverseSubArray(arr, begin, end) {
  while (begin < end) {
    const tmp = arr[begin]
    arr[begin] = arr[end]
    arr[end] = tmp
    ++begin
    --end
  }
}

/**
   * @param {Array<VALUE>} arr The array to modify.
   * @param {!Array<VALUE>|VALUE} data The elements or arrays of elements to add to arr.
   * @template VALUE
   */
export function extend(arr, data) {
  const extension = Array.isArray(data) ? data : [data]
  const length = extension.length
  for (let i = 0; i < length; i++) {
    arr[arr.length] = extension[i]
  }
}

/**
   * @param {Array<VALUE>} arr The array to modify.
   * @param {VALUE} obj The element to remove.
   * @template VALUE
   * @return {boolean} If the element was removed.
   */
export function remove(arr, obj) {
  const i = arr.indexOf(obj)
  const found = i > -1
  if (found) {
    arr.splice(i, 1)
  }
  return found
}

/**
   * @param {Array<VALUE>} arr The array to search in.
   * @param {function(VALUE, number, ?) : boolean} func The function to compare.
   * @template VALUE
   * @return {VALUE|null} The element found or null.
   */
export function find(arr, func) {
  const length = arr.length >>> 0
  let value

  for (let i = 0; i < length; i++) {
    value = arr[i]
    if (func(value, i, arr)) {
      return value
    }
  }
  return null
}

/**
   * @param {Array|Uint8ClampedArray} arr1 The first array to compare.
   * @param {Array|Uint8ClampedArray} arr2 The second array to compare.
   * @return {boolean} Whether the two arrays are equal.
   */
export function equals(arr1, arr2) {
  const len1 = arr1.length
  if (len1 !== arr2.length) {
    return false
  }
  for (let i = 0; i < len1; i++) {
    if (arr1[i] !== arr2[i]) {
      return false
    }
  }
  return true
}

/**
   * Sort the passed array such that the relative order of equal elements is preverved.
   * See https://en.wikipedia.org/wiki/Sorting_algorithm#Stability for details.
   * @param {Array<*>} arr The array to sort (modifies original).
   * @param {!function(*, *): number} compareFnc Comparison function.
   * @api
   */
export function stableSort(arr, compareFnc) {
  const length = arr.length
  const tmp = Array(arr.length)
  let i
  for (i = 0; i < length; i++) {
    tmp[i] = {'index': i, 'value': arr[i]}
  }
  tmp.sort(function (a, b) {
    return compareFnc(a.value, b.value) || a.index - b.index
  })
  for (i = 0; i < arr.length; i++) {
    arr[i] = tmp[i].value
  }
}

/**
   * @param {Array<*>} arr The array to search in.
   * @param {Function} func Comparison function.
   * @return {number} Return index.
   */
export function findIndex(arr, func) {
  let index
  const found = !arr.every(function (el, idx) {
    index = idx
    return !func(el, idx, arr)
  })
  return found ? index : -1
}

/**
   * @param {Array<*>} arr The array to test.
   * @param {Function=} opt_func Comparison function.
   * @param {boolean=} opt_strict Strictly sorted (default false).
   * @return {boolean} Return index.
   */
export function isSorted(arr, opt_func, opt_strict) {
  const compare = opt_func || numberSafeCompareFunction
  return arr.every(function (currentVal, index) {
    if (index === 0) {
      return true
    }
    const res = compare(arr[index - 1], currentVal)
    return !(res > 0 || opt_strict && res === 0)
  })
}